Editing Shape Transforms
This programming recipe changes the behavior of the application created in the previous three recipes. With the code from this recipe, you allow users to scale a path shape.When the user selects a path shape, the code from this recipe provides transform control handles rather than geometry control handles. When the
user presses the mouse button on a transform control handle, the code tracks the mouse movement, providing feedback as the user drags the mouse by scaling the path shape.Figure 6-4 depicts this transform-editing mechanism.
Figure 6-4 Scaling a shape with transform control handles
Overview of Recipe Steps
The steps in this recipe show you how to:
You need to follow each step in this recipe to provide transform controls and dragging feedback for path shapes.
- Create transform control handles
- Provide feedback by scaling the path
- Prepare to provide transform feedback
- Scale the path as the user drags the mouse
- Edit selected path transform
Functions Used in This Recipe
QuickDraw GX functions used in this recipe:
GXNewPolygons
"Geometric Shapes"
QuickDraw GX GraphicsGXSetShapeFill
"Shape Objects"
QuickDraw GX ObjectsGXSetShapeHitTest
"Shape Objects"
QuickDraw GX ObjectsGXGetShapeLocalBounds
"View-Related Objects"
QuickDraw GX ObjectsGXGetShapePoints
"Geometric Shapes"
QuickDraw GX GraphicsGXSetShapeType
"Shape Objects"
QuickDraw GX ObjectsGXDisposeShape
"Shape Objects"
QuickDraw GX ObjectsGXCopyToShape
"Shape Objects"
QuickDraw GX ObjectsGXMoveShapeTo
"Transform Objects"
QuickDraw GX ObjectsGXRotateShape
"Transform Objects"
QuickDraw GX ObjectsGXGetShapeMapping
"Transform Objects"
QuickDraw GX ObjectsGXGetViewPortMouse
"QuickDraw GX and the
Macintosh Environment"
QuickDraw GX Environment and UtilitiesFixedDivide
"QuickDraw GX Mathematics"
QuickDraw GX Environment and UtilitiesGXSetShapeMapping
"Transform Objects"
QuickDraw GX ObjectsGXScaleShape
"Transform Objects"
QuickDraw GX Objects"GXSetShapeAttributes
"Shape Objects"
QuickDraw GX ObjectsGXScaleShape
"Transform Objects"
QuickDraw GX ObjectsGXSetShapeAttributes
"Shape Objects"
QuickDraw GX ObjectsGXIgnoreGraphicsNotice
"Errors, Warnings, and Notices"
QuickDraw GX Environment and UtilitiesGXPopGraphicsNotice
"Errors, Warnings, and Notices"
QuickDraw GX Environment and UtilitiesGXDrawShape
"Shape Objects"
QuickDraw GX ObjectsStandard Macintosh functions used in this recipe:
StillDown
"Event Manager"
Macintosh Toolbox EssentialsThis recipe gives a brief description of these functions; you can find complete reference information for these functions in the Inside Macintosh suite of books.
This function also uses a function from the QuickDraw GX libraries:
SetShapeFastXorTransfer
transferMode library Recipe Step Descriptions
In this section, each step is described individually.
- Create transform control handles
The first thing you need to do is replace the definition of the
MyCreatePathControlHandles
function originally defined on page 197.
The original version of this function created geometry control handles,
while the new version creates transform control handles. In the new version, you need these local variables:
int index;
gxShape aScaleHandle;
gxPoint cornerPoints[4];
static long scaleHandle[] = {1, /* number of contours */
4, /* number of points */
ff(0), ff(0),
fl(5.5), ff(0),
ff(0), fl(5.5),
ff(0), ff(0)};The
cornerPoints
array stores the positions of the four corners of the bounding rectangle of the selected shape.The
scaleHandle
array specifies the geometry of the small, triangular transform control handles.The definition of the
scaleHandle
array uses thefl
macro. This macro is similar to theff
macro, except thefl
macro converts a floating-point number (like 5.5) to a fixed-point number.You can use the
scaleHandle
array to create a polygon shape using
this code:
aScaleHandle = GXNewPolygons((gxPolygons *) scaleHandle);
GXSetShapeFill(aScaleHandle, gxWindingFill);
SetShapeFastXorTransfer(aScaleHandle, &gBlack, &gWhite);
GXSetShapeHitTest(aHandle, gxBoundsPart, ff(1));The transform control handle polygon has a solid shape fill, an exclusive-OR transfer mode, and is hit-tested only on the bounds part.
Now, you need to turn the single transform control handle into four--one for each corner of the path shape's bounding rectangle by calling
MyFindCorners(pathToEdit, cornerPoints);The
MyFindCorners
function uses theGXGetShapeLocalBounds
function
to find the path's bounding rectangle and then it convertsthe bounding rectangle to a polygon, which changes the geometry from having two points--the upper-left and lower-right corners--to having four points--one for each corner. The function then uses the
GXGetShapePoints
function to copy those points into an array of points and then dispose of the bounding rectangle shape.
void MyFindCorners(gxShape aShape, gxPoint *cornerPoints)
{
gxRectangle bounds;
gxShape boundsShape;
GXGetShapeLocalBounds(aShape, &bounds);
boundsShape = GXNewRectangle(&bounds);
GXSetShapeType(boundsShape, gxPolygonType);
GXGetShapePoints(boundsShape, 1, 4, cornerPoints);
GXDisposeShape(boundsShape);
}Once you've found the four corner points, you can create the four transform control handles. First, make a copy of the transform control handle you created earlier and move the copy to the upper-left corner:
gCurrent->controls[0] = GXCopyToShape(nil, aScaleHandle);
GXMoveShapeTo(gCurrent->controls[0],
cornerPoints[0].x - ff(5),
cornerPoints[0].y - ff(5));To make the transform control handle for the upper-right corner, you rotate the original and then copy it and move it to the upper-right corner:
GXRotateShape(aScaleHandle, ff(90), ff(0), ff(0));
gCurrent->controls[1] = GXCopyToShape(nil, aScaleHandle);
GXMoveShapeTo(gCurrent->controls[1],
cornerPoints[1].x + ff(5),
cornerPoints[1].y - ff(5));You create the lower-right and lower-left transform control handles similarly:
GXRotateShape(aScaleHandle, ff(90), ff(0), ff(0));
gCurrent->controls[2] = GXCopyToShape(nil, aScaleHandle);
GXMoveShapeTo(gCurrent->controls[2],
cornerPoints[2].x + ff(5),
cornerPoints[2].y + ff(5));
GXRotateShape(aScaleHandle, ff(90), ff(0), ff(0));
gCurrent->controls[3] = GXCopyToShape(nil, aScaleHandle);
GXMoveShapeTo(gCurrent->controls[0],
cornerPoints[3].x - ff(5),
cornerPoints[3].y + ff(5));Notice that each control handle is moved 5 points away from the real corners of the path shape--to give a little room between the handles and
the path.After you've created the four copies of the transform control handle, you can dispose of the original one:
GXDisposeShape(aScaleHandle);- Provide feedback by scaling the path
The other function you need to replace is the
MyPathDrag
function, originally defined on page 202. The original version of this function edits the path geometry while the user dragged the mouse. The new version edits the transform. Here is the flow of control for this function:
void MyPathDrag(gxPoint *originalPoint, int whichControl)
{
/* Declare local variables -- see Step 3. */
/* Prepare for loop -- see Step 3. */
do {
/* Provide feedback -- see Step 4. */
} while (StillDown());
/* Edit transform of selected path -- see Step 5. */
}The next few steps--Steps 3 through 5--show how to implement the parts of the new version of the
MyPathDrag
function.- Prepare to provide transform feedback
The new
MyPathDrag
function scales the shape as the user drags the mouse. When you scale a shape with QuickDraw GX, you specify a scaling origin--the point that doesn't move as the shape is scaled. In this recipe, you define the scaling origin as the corner opposite from the corner on which the user pressed the mouse button. To calculate the origin, you'll have to find the four corner points of the path shape's bounding rectangle and then calculate which point is opposite the selected point. You can store the four corner points in this array:
gxPoint cornerPoints[4];You store the origin point, once you've calculated it, in this point structure:
gxPoint originPoint;Like the original version, the new version of the
MyPathDrag
function needs to keep track of the previous mouse position and the new mouse position as the user drags the mouse. You can use these point structures to store this information:
gxPoint oldPoint,
newPoint;As the user drags the mouse, the
MyPathDrag
function calculates how much to scale the path shape based on its original height and width (the height and width of the path shape when the user started pressed on the mouse button) and its new height and width (the height and width as indicated
by the current mouse position). You can store these values in fixed-point variables:
Fixed originalHeight, originalWidth,
newHeight, newWidth;
Fixed hScaleFactor = fixed1,
vScaleFactor = fixed1;The
MyPathDrag
function also needs to store the original mapping property of the transform object of the selected path shape:
gxMapping originalMapping;You also need to create two path shapes to continually redraw as the user drags the mouse:
gxShape oldPath = nil,
newPath = nil;The
MyPathDrag
function begins by erasing the transform control handles:
MyErasePathControlHandles();Then, the function finds the four corners of the bounding rectangle of
the path shape and then calculates the scaling origin point as the corner opposite the one the user pressed on:
MyFindCorners(gCurrent->selectedPath, cornerPoints);
originPoint.x = cornerPoints[(whichControl+1)%4].x;
originPoint.y = cornerPoints[(whichControl+1)%4].y;(Note that the
whichControl
variable is 1-based--that is, it has a value between 1 and 4. ThecornerPoints
array is 0-based--that is, its indexes
are numbered from 0 to 3. Therefore, you add 1 to thewhichControl
value rather than 2, as you might expect.)The function then stores a copy of the path shape's mapping:
GXGetShapeMapping(gCurrent->selectedPath, &originalMapping);The
oldPoint
variable, which represents the previous mouse position, is initialized to be the original point the user pressed on:
oldPoint.x = originalPoint->x;
oldPoint.y = originalPoint->y;The path shapes used to provide feedback are initialized to be copies of the selected path shape:
oldPath = GXCopyToShape(nil, gCurrent->selectedPath);
SetShapeFastXorTransfer(oldPath, &gBlack, &gWhite);
newPath = GXCopyToShape(nil, oldPath);Notice that the transfer mode of the feedback path shapes is set to the exclusive-OR mode to provide for fast erasing and redrawing.
- Scale the path as the user drags the mouse
The new version of the
MyPathDrag
function also uses ado
loop to provide feedback while the user is dragging the mouse:
do {
GXGetViewPortMouse(gCurrent->viewPort, &newPoint);
if (MyMouseMoved(newPoint, oldPoint)) {
/* edit new path to reflect new mouse position */
/* erase old path */
/* draw new path */
/* set up for next pass through do loop */
}
} while (StillDown());As in the previous version of the My
PathDrag
function, the code in thedo
loop needs to edit thenewPath
path shape to reflect the new mouse position, erase theoldPath
path shape, draw thenewPath
path shape, and set up for the next pass through the loop.To edit the new path shape to reflect the new mouse position, you first need to determine the scaling factors, using this code:
newWidth = MyFixedAbs(newPoint.x - originPoint.x);
newHeight = MyFixedAbs(newPoint.y - originPoint.y);
newWidth = MyFixedMax(ff(1), newWidth);
newHeight = MyFixedMax(ff(1), newHeight);
hScaleFactor = FixedDivide(newWidth, originalWidth);
vScaleFactor = FixedDivide(newHeight, originalHeight);QuickDraw GX provides the
FixedDivide
function; you need to provide
theMyFixedAbs
function (which finds the absolute value of a fixed-point number) and theMyFixedMax
function (which returns the larger of two fixed-point numbers):
#define MyFixedAbs(a) ((a) >= 0 ? (a) : -(a))
#define MyFixedMax(a, b) ((a) >= (b) ? (a) : (b))You also need to adjust the scaling factors, depending on whether the user has dragged the mouse clear past the origin point. You can use this code:
if ((newPoint.x < originPoint.x) ^
(originalPoint->x < originPoint.x))
hScaleFactor = -hScaleFactor;
if ((newPoint.y < originPoint.y) ^
(originalPoint->y < originPoint.y))
vScaleFactor = -vScaleFactor;Once you have the scaling factors, you set the mapping of the new path back to the original mapping and then use the
GXScaleShape
function to scale the mapping to reflect the new mouse position:
GXSetShapeMapping(newPath, &originalMapping);
GXScaleShape(newPath,
hScaleFactor, vScaleFactor,
originPoint.x, originPoint.y);The
GXScaleShape
function can affect the mapping property of a shape's transform object or the actual geometric points of the shape's geometry directly, depending on whether you have set the map transform shape attribute of your path shapes. If you add the following line of code to yourMyHandleCreatePath
function, then all of your path shapes have this attribute set and the shape's transform object is affected, not the shape's geometry itself. (Even the feedback path shapes--oldPath
andnewPath
-- have this attribute set, because they were originally created as copies of the selected shape.)
GXSetShapeAttributes(gCurrent->selectedPath,
gxMapTransformShape);As you debug your sample program, you may find that the
GXSetShapeMapping
function posts amapping_already_set
notice
the first time you call it. You can suppress this notice using theGXIgnoreGraphicsNotice
andGXPopGraphicsNotice
functions, as in
this code:
GXIgnoreGraphicsNotice(mapping_already_set);
GXSetShapeMapping(newPath, &originalMapping);
GXPopGraphicsNotice();Once you've edited the transform of the new path, you can erase the old path and redraw the new path:
GXDrawShape(oldPath);
GXDrawShape(newPath);Finally, you can set up the variables for the next pass through the loop:
GXCopyToShape(oldPath, newPath);
oldPoint = newPoint;- Edit the selected path transform
When the user releases the mouse button, you can dispose of the two feedback path shapes:
GXDisposeShape(oldPath);
GXDisposeShape(newPath);Now you can edit the mapping property of the real path shape's transform object. You can use the
hScaleFactor
andvScaleFactor
variables that were set in thedo
loop--they still have the most current values when you exit the loop:
GXScaleShape(gCurrent->selectedPath,
hScaleFactor, vScaleFactor,
originPoint.x, originPoint.y);Finally, you need to redraw the transform control handles and update the contents of the window:
MyDisposePathControlHandles();
MyCreatePathControlHandles();
MyDrawWindow();
Related Recipes
The previous three recipes, "Creating Geometric Shapes" beginning on page 179, "Selecting Shapes" beginning on page 193, and "Editing Shape Geometries" beginning on page 199, show how to allow the user to create, select, and edit path shape geometries. You should be familiar with the steps
of those recipes before using this recipe.The next recipe, "Dragging Shapes Using Offscreen Bitmaps," shows how to provide a implement feedback in a different way. The code in that recipe allows a user to drag colored circles around a window. It uses offscreen buffering to provide smooth redrawing.
The recipes in Chapter 4, "Using the QuickDraw GX Environment," show you how to initialize QuickDraw GX and set up the QuickDraw GX debugging facilities. You should read the recipes in that chapter before using any recipes in this chapter.
The recipes in Chapter 5, "Using Macintosh Windows," show you how to create Macintosh windows, attach QuickDraw GX view ports to them, and implement zooming, resizing, and scrolling. You need to be familiar with the information in that chapter before you can display QuickDraw GX graphics in a Macintosh window.
Main | Page One | What's New | Apple Computer, Inc. | Find It | Contact Us | Help